/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
using System;
using java = biz.ritter.javapi;

using org.apache.commons.codec;

namespace org.apache.commons.codec.language
{

    /**
     * Encodes a string into a metaphone value. 
     * <p>
     * Initial Java implementation by <CITE>William B. Brogden. December, 1997</CITE>. 
     * Permission given by <CITE>wbrogden</CITE> for code to be used anywhere.
     * </p>
     * <p>
     * <CITE>Hanging on the Metaphone</CITE> by <CITE>Lawrence Philips</CITE> in <CITE>Computer Language of Dec. 1990, p
     * 39.</CITE>
     * </p>
     * <p>
     * Note, that this does not match the algorithm that ships with PHP, or the algorithm 
     * found in the Perl <a href="http://search.cpan.org/~mschwern/Text-Metaphone-1.96/Metaphone.pm">Text:Metaphone-1.96</a>.
     * They have had undocumented changes from the originally published algorithm. 
     * For more information, see <a href="https://issues.apache.org/jira/browse/CODEC-57">CODEC-57</a>.
     * </p>
     * 
     * @author Apache Software Foundation
     * @version $Id: Metaphone.java 797690 2009-07-24 23:28:35Z ggregory $
     */
    public class Metaphone : StringEncoder
    {

        /**
         * Five values in the English language 
         */
        private const String VOWELS = "AEIOU";

        /**
         * Variable used in Metaphone algorithm
         */
        private const String FRONTV = "EIY";

        /**
         * Variable used in Metaphone algorithm
         */
        private const String VARSON = "CSPTG";

        /**
         * The max code length for metaphone is 4
         */
        private int maxCodeLen = 4;

        /**
         * Creates an instance of the Metaphone encoder
         */
        public Metaphone() :
            base()
        {
        }

        /**
         * Find the metaphone value of a String. This is similar to the
         * soundex algorithm, but better at finding similar sounding words.
         * All input is converted to upper case.
         * Limitations: Input format is expected to be a single ASCII word
         * with only characters in the A - Z range, no punctuation or numbers.
         *
         * @param txt String to find the metaphone code for
         * @return A metaphone code corresponding to the String supplied
         */
        public String metaphone(String txt)
        {
            bool hard = false;
            if ((txt == null) || (txt.length() == 0))
            {
                return "";
            }
            // single character is itself
            if (txt.length() == 1)
            {
                return txt.toUpperCase(java.util.Locale.ENGLISH);
            }

            char[] inwd = txt.toUpperCase(java.util.Locale.ENGLISH).toCharArray();

            java.lang.StringBuffer local = new java.lang.StringBuffer(40); // manipulate
            java.lang.StringBuffer code = new java.lang.StringBuffer(10); //   output
            // handle initial 2 characters exceptions
            switch (inwd[0])
            {
                case 'K':
                case 'G':
                case 'P': /* looking for KN, etc*/
                    if (inwd[1] == 'N')
                    {
                        local.append(inwd, 1, inwd.Length - 1);
                    }
                    else
                    {
                        local.append(inwd);
                    }
                    break;
                case 'A': /* looking for AE */
                    if (inwd[1] == 'E')
                    {
                        local.append(inwd, 1, inwd.Length - 1);
                    }
                    else
                    {
                        local.append(inwd);
                    }
                    break;
                case 'W': /* looking for WR or WH */
                    if (inwd[1] == 'R')
                    {   // WR -> R
                        local.append(inwd, 1, inwd.Length - 1);
                        break;
                    }
                    if (inwd[1] == 'H')
                    {
                        local.append(inwd, 1, inwd.Length - 1);
                        local.setCharAt(0, 'W'); // WH -> W
                    }
                    else
                    {
                        local.append(inwd);
                    }
                    break;
                case 'X': /* initial X becomes S */
                    inwd[0] = 'S';
                    local.append(inwd);
                    break;
                default:
                    local.append(inwd);
                    break;
            } // now local has working string with initials fixed

            int wdsz = local.length();
            int n = 0;

            while ((code.length() < this.getMaxCodeLen()) &&
                   (n < wdsz))
            { // max code size of 4 works well
                char symb = local.charAt(n);
                // remove duplicate letters except C
                if ((symb != 'C') && (isPreviousChar(local, n, symb)))
                {
                    n++;
                }
                else
                { // not dup
                    switch (symb)
                    {
                        case 'A':
                        case 'E':
                        case 'I':
                        case 'O':
                        case 'U':
                            if (n == 0)
                            {
                                code.append(symb);
                            }
                            break; // only use vowel if leading char
                        case 'B':
                            if (isPreviousChar(local, n, 'M') &&
                                 isLastChar(wdsz, n))
                            { // B is silent if word ends in MB
                                break;
                            }
                            code.append(symb);
                            break;
                        case 'C': // lots of C special cases
                            /* discard if SCI, SCE or SCY */
                            if (isPreviousChar(local, n, 'S') &&
                                 !isLastChar(wdsz, n) &&
                                 (FRONTV.indexOf(local.charAt(n + 1)) >= 0))
                            {
                                break;
                            }
                            if (regionMatch(local, n, "CIA"))
                            { // "CIA" -> X
                                code.append('X');
                                break;
                            }
                            if (!isLastChar(wdsz, n) &&
                                (FRONTV.indexOf(local.charAt(n + 1)) >= 0))
                            {
                                code.append('S');
                                break; // CI,CE,CY -> S
                            }
                            if (isPreviousChar(local, n, 'S') &&
                                isNextChar(local, n, 'H'))
                            { // SCH->sk
                                code.append('K');
                                break;
                            }
                            if (isNextChar(local, n, 'H'))
                            { // detect CH
                                if ((n == 0) &&
                                    (wdsz >= 3) &&
                                    isVowel(local, 2))
                                { // CH consonant -> K consonant
                                    code.append('K');
                                }
                                else
                                {
                                    code.append('X'); // CHvowel -> X
                                }
                            }
                            else
                            {
                                code.append('K');
                            }
                            break;
                        case 'D':
                            if (!isLastChar(wdsz, n + 1) &&
                                isNextChar(local, n, 'G') &&
                                (FRONTV.indexOf(local.charAt(n + 2)) >= 0))
                            { // DGE DGI DGY -> J 
                                code.append('J'); n += 2;
                            }
                            else
                            {
                                code.append('T');
                            }
                            break;
                        case 'G': // GH silent at end or before consonant
                            if (isLastChar(wdsz, n + 1) &&
                                isNextChar(local, n, 'H'))
                            {
                                break;
                            }
                            if (!isLastChar(wdsz, n + 1) &&
                                isNextChar(local, n, 'H') &&
                                !isVowel(local, n + 2))
                            {
                                break;
                            }
                            if ((n > 0) &&
                                (regionMatch(local, n, "GN") ||
                                  regionMatch(local, n, "GNED")))
                            {
                                break; // silent G
                            }
                            if (isPreviousChar(local, n, 'G'))
                            {
                                // NOTE: Given that duplicated chars are removed, I don't see how this can ever be true
                                hard = true;
                            }
                            else
                            {
                                hard = false;
                            }
                            if (!isLastChar(wdsz, n) &&
                                (FRONTV.indexOf(local.charAt(n + 1)) >= 0) &&
                                (!hard))
                            {
                                code.append('J');
                            }
                            else
                            {
                                code.append('K');
                            }
                            break;
                        case 'H':
                            if (isLastChar(wdsz, n))
                            {
                                break; // terminal H
                            }
                            if ((n > 0) &&
                                (VARSON.indexOf(local.charAt(n - 1)) >= 0))
                            {
                                break;
                            }
                            if (isVowel(local, n + 1))
                            {
                                code.append('H'); // Hvowel
                            }
                            break;
                        case 'F':
                        case 'J':
                        case 'L':
                        case 'M':
                        case 'N':
                        case 'R':
                            code.append(symb);
                            break;
                        case 'K':
                            if (n > 0)
                            { // not initial
                                if (!isPreviousChar(local, n, 'C'))
                                {
                                    code.append(symb);
                                }
                            }
                            else
                            {
                                code.append(symb); // initial K
                            }
                            break;
                        case 'P':
                            if (isNextChar(local, n, 'H'))
                            {
                                // PH -> F
                                code.append('F');
                            }
                            else
                            {
                                code.append(symb);
                            }
                            break;
                        case 'Q':
                            code.append('K');
                            break;
                        case 'S':
                            if (regionMatch(local, n, "SH") ||
                                regionMatch(local, n, "SIO") ||
                                regionMatch(local, n, "SIA"))
                            {
                                code.append('X');
                            }
                            else
                            {
                                code.append('S');
                            }
                            break;
                        case 'T':
                            if (regionMatch(local, n, "TIA") ||
                                regionMatch(local, n, "TIO"))
                            {
                                code.append('X');
                                break;
                            }
                            if (regionMatch(local, n, "TCH"))
                            {
                                // Silent if in "TCH"
                                break;
                            }
                            // substitute numeral 0 for TH (resembles theta after all)
                            if (regionMatch(local, n, "TH"))
                            {
                                code.append('0');
                            }
                            else
                            {
                                code.append('T');
                            }
                            break;
                        case 'V':
                            code.append('F'); break;
                        case 'W':
                        case 'Y': // silent if not followed by vowel
                            if (!isLastChar(wdsz, n) &&
                                isVowel(local, n + 1))
                            {
                                code.append(symb);
                            }
                            break;
                        case 'X':
                            code.append('K'); code.append('S');
                            break;
                        case 'Z':
                            code.append('S'); break;
                    } // end switch
                    n++;
                } // end else from symb != 'C'
                if (code.length() > this.getMaxCodeLen())
                {
                    code.setLength(this.getMaxCodeLen());
                }
            }
            return code.toString();
        }

        private bool isVowel(java.lang.StringBuffer str, int index)
        {
            return VOWELS.indexOf(str.charAt(index)) >= 0;
        }

        private bool isPreviousChar(java.lang.StringBuffer str, int index, char c)
        {
            bool matches = false;
            if (index > 0 &&
                index < str.length())
            {
                matches = str.charAt(index - 1) == c;
            }
            return matches;
        }

        private bool isNextChar(java.lang.StringBuffer str, int index, char c)
        {
            bool matches = false;
            if (index >= 0 &&
                index < str.length() - 1)
            {
                matches = str.charAt(index + 1) == c;
            }
            return matches;
        }

        private bool regionMatch(java.lang.StringBuffer str, int index, String test)
        {
            bool matches = false;
            if (index >= 0 &&
                (index + test.length() - 1) < str.length())
            {
                String substring = str.substring(index, index + test.length());
                matches = substring.equals(test);
            }
            return matches;
        }

        private bool isLastChar(int wdsz, int n)
        {
            return n + 1 == wdsz;
        }


        /**
         * Encodes an Object using the metaphone algorithm.  This method
         * is provided in order to satisfy the requirements of the
         * Encoder interface, and will throw an EncoderException if the
         * supplied object is not of type java.lang.String.
         *
         * @param pObject Object to encode
         * @return An object (or type java.lang.String) containing the 
         *         metaphone code which corresponds to the String supplied.
         * @throws EncoderException if the parameter supplied is not
         *                          of type java.lang.String
         */
        public Object encode(Object pObject)
        {//throws EncoderException {
            if (pObject is java.lang.StringJ) pObject = ((java.lang.StringJ)pObject).ToString();
            if (!(pObject is String))
            {
                throw new EncoderException("Parameter supplied to Metaphone encode is not of type java.lang.String");
            }
            return metaphone((String)pObject);
        }

        /**
         * Encodes a String using the Metaphone algorithm. 
         *
         * @param pString String object to encode
         * @return The metaphone code corresponding to the String supplied
         */
        public String encode(String pString)
        {
            return metaphone(pString);
        }

        /**
         * Tests is the metaphones of two strings are identical.
         *
         * @param str1 First of two strings to compare
         * @param str2 Second of two strings to compare
         * @return <code>true</code> if the metaphones of these strings are identical, 
         *        <code>false</code> otherwise.
         */
        public bool isMetaphoneEqual(String str1, String str2)
        {
            return metaphone(str1).equals(metaphone(str2));
        }

        /**
         * Returns the maxCodeLen.
         * @return int
         */
        public int getMaxCodeLen() { return this.maxCodeLen; }

        /**
         * Sets the maxCodeLen.
         * @param maxCodeLen The maxCodeLen to set
         */
        public void setMaxCodeLen(int maxCodeLen) { this.maxCodeLen = maxCodeLen; }

    }
}